package no.sachmuld.methodcombination.impl.annotation;

import no.sachmuld.methodcombination.api.reflection.component.MethodCombination;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;

/**
 *
 */
public class MethodCombinationInitializationUtils {

    public static MethodCombination initialize(Method method) throws AmbiguousMethodCombinationException, MethodCombinationDependencyException {

        Set<Annotation> roots = findRootAnnotations(method);

        if (roots.size() == 0) {
            return null;
        }

        Map<Class<? extends MethodCombination>, MethodCombination> instanceMap = new HashMap<>();

        MethodCombination methodCombination = null;
        int n = roots.size();
        for (int i = 0; i < n; i++) {

            methodCombination = initializeNextMethodCombination(roots, instanceMap);
            if (methodCombination == null) {
                throw new MethodCombinationDependencyException();
            }

        }

        if (roots.size() != 1) {
            throw new AmbiguousMethodCombinationException();
        }

        return methodCombination;

    }

    private static Set<Annotation> findRootAnnotations(Method method) {
        Set<Annotation> roots = new HashSet<>();
        for (Annotation a : method.getAnnotations()) {
            MethodCombinationImplementation methodCombinationImplementation = findMethodCombinationImplementation(a);
            if (methodCombinationImplementation != null) {
                roots.add(a);
            }
        }
        return roots;
    }

    private static MethodCombination initializeNextMethodCombination(Set<Annotation> roots, Map<Class<? extends MethodCombination>, MethodCombination> instanceMap) throws AmbiguousMethodCombinationException, MethodCombinationDependencyException {

        for (Annotation currentAnnotaton : roots) {

            MethodCombination methodCombination = initializeNextMethodCombinationIfPossible(currentAnnotaton, roots, instanceMap);
            if (methodCombination != null) {
                return methodCombination;
            }
        }

        return null;

    }

    private static MethodCombination initializeNextMethodCombinationIfPossible(Annotation currentAnnotaton, Set<Annotation> roots, Map<Class<? extends MethodCombination>, MethodCombination> instanceMap) throws AmbiguousMethodCombinationException, MethodCombinationDependencyException {

        Class<? extends MethodCombination> methodCombinationClass = findMethodCombinationClass(currentAnnotaton);

        if (instanceMap.containsKey(methodCombinationClass)) {
            // already initialized
            return null;
        }

        Set<Annotation> dependencies = new HashSet<>();
        Map<Class<? extends MethodCombination>, MethodCombination> dependencyMap = new HashMap<>();

        for (Class<? extends MethodCombination> dependencyClass : findNestedMethodCombinationClasses(currentAnnotaton)) {

            Annotation dependency = resolveDependency(currentAnnotaton, dependencyClass, roots, instanceMap);

            if (dependency != null) {
                dependencies.add(dependency);
                dependencyMap.put(dependencyClass, instanceMap.get(findMethodCombinationClass(dependency)));
            } else {
                // dependencies could not be resolved at this point
                return null;
            }
        }

        try {
            MethodCombination instance = methodCombinationClass.newInstance();
            instanceMap.put(methodCombinationClass, instance);
            MethodCombinationInitializer initializer = findMethodCombinationInitializerClass(currentAnnotaton).newInstance();
            initializer.initialize(instance, currentAnnotaton, dependencyMap);

            for (Annotation dependency : dependencies) {
                roots.remove(dependency);
            }

            instanceMap.put(methodCombinationClass, instance);

            return instance;

        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

    }

    private static Annotation resolveDependency(Annotation currentAnnotation, Class<? extends MethodCombination> dependencyClass, Set<Annotation> roots, Map<Class<? extends MethodCombination>, MethodCombination> instanceMap) {
        Annotation matchingDependency = null;
        for (Annotation rootAnnotation : roots) {
            if (rootAnnotation != currentAnnotation) {

                Class<? extends MethodCombination> rootClass = findMethodCombinationClass(rootAnnotation);
                if (dependencyClass.isAssignableFrom(rootClass)) {
                    if (matchingDependency != null) {
                        // ambiguous
                        return null;
                    }
                    MethodCombination rootInstance = instanceMap.get(rootClass);
                    if (rootInstance == null) {
                        // not initialized
                        return null;
                    } else {
                        matchingDependency = rootAnnotation;
                    }
                }
            }
        }
        return matchingDependency;
    }

    private static Class<? extends MethodCombination> findMethodCombinationClass(Annotation a) {
        MethodCombinationImplementation methodCombinationImplementation = findMethodCombinationImplementation(a);
        return methodCombinationImplementation.value();
    }

    private static Class<? extends MethodCombination>[] findNestedMethodCombinationClasses(Annotation a) {
        MethodCombinationImplementation methodCombinationImplementation = findMethodCombinationImplementation(a);
        return methodCombinationImplementation.nested();
    }

    private static Class<? extends MethodCombinationInitializer> findMethodCombinationInitializerClass(Annotation a) {
        MethodCombinationImplementation methodCombinationImplementation = findMethodCombinationImplementation(a);
        return methodCombinationImplementation.initializer();
    }

    private static MethodCombinationImplementation findMethodCombinationImplementation(Annotation a) {
        MethodCombinationImplementation methodCombinationImplementation = a.annotationType().getAnnotation(MethodCombinationImplementation.class);
        if (methodCombinationImplementation != null) {
            return methodCombinationImplementation;
        }
        return null;
    }
}
